Return ASCOM INVALID_VALUE for out-of-range integer parameters#14
Return ASCOM INVALID_VALUE for out-of-range integer parameters#14ivonnyssen wants to merge 7 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
This PR updates Alpaca server parameter parsing to distinguish unparseable integer inputs (HTTP 400) from parseable but out-of-range integer inputs (ASCOM INVALID_VALUE / 1025), and ensures u32 request parameters can exceed i32::MAX by widening to an intermediate signed type first.
Changes:
- Introduces a custom serde
Deserializerfor request parameters that parses integers via an intermediate type, enabling explicit range-error classification. - Adds a new
Error::ParameterOutOfRangeand maps it to ASCOMINVALID_VALUEin transaction-wrapped responses. - Adds unit tests for integer parsing/range behavior in
params.rs.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
src/server/params.rs |
Replaces serde_plain::from_str parsing with a custom deserializer and adds tests for integer parsing and range classification. |
src/server/error.rs |
Adds a new ParameterOutOfRange error variant and updates HTTP status mapping in IntoResponse. |
src/server/response.rs |
Maps ParameterOutOfRange to an ASCOM INVALID_VALUE response (HTTP 200) when returning ResponseWithTransaction<Result<T>>. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Analysis of Copilot review comments 3 & 4 (out-of-range parameters escaping the transaction wrapper)Updated 2026-04-21: code snippet refreshed to match the new Copilot flagged that out-of-range integer errors during How the issue arises
So sending e.g. The device parameters that ConformU actually tests (like Proposed fixCatch the out-of-range case in // mod.rs — add to imports:
use super::params::AlpacaParseError;
use crate::ASCOMError;
// mod.rs — replace line 106:
// let request_transaction = RequestTransaction::extract(&mut self.params)?;
// with:
let request_transaction = match RequestTransaction::extract(&mut self.params) {
Ok(t) => t,
Err(Error::BadParameter {
name,
err: AlpacaParseError::OutOfRange { value, target_type },
}) => {
return Ok(ResponseWithTransaction {
transaction: ResponseTransaction::new(None),
response: Err::<(), _>(ASCOMError::invalid_value(format!(
"Parameter {name:?} value {value} is out of range for {target_type}"
))),
}
.into_response());
}
Err(e) => return Err(e.into()),
};This works because I also considered a two-phase extraction approach (making Let me know if you'd like this fix included in the PR or if you'd prefer to leave it for a follow-up. — Igor (via Claude Code) |
RReverser
left a comment
There was a problem hiding this comment.
uint32 overflow: ClientID and ClientTransactionID are uint32 per spec, so values above i32::MAX (e.g. 2147483648) must parse successfully. The previous code used serde_plain::from_str which parses directly to the target type with no intermediate widening, so valid uint32 values were rejected. Using i64 as the intermediate type covers the full uint32 range.
I've been re-reading this several times and I don't get it. How is parsing directly to the target type (u32) a bad thing, but using an unrelated i64 type a good thing?
|
@RReverser you're right — the PR description framing was wrong and I've rewritten it. The i64 intermediate isn't about widening u32; it's the narrowest type that covers the full range of every Alpaca numeric target (i8/i16/i32/u8/u16/u32/usize). That lets a single generic I've also addressed your three inline comments in the latest push:
|
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Alpaca is a proper noun, not an acronym. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Make AlpacaParseError pub(crate) and carry it directly on Error::BadParameter. The ResponseWithTransaction<Result<T>> IntoResponse impl now destructures the inner AlpacaParseError::OutOfRange to produce ASCOM INVALID_VALUE, while plain BadFormat errors continue to return HTTP 400. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The custom AlpacaDeserializer now handles all Alpaca parameter parsing, so the serde_plain helper crate is no longer needed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
When a `Deserialize_repr` enum (e.g. `DriveRate`) receives an integer
that's syntactically valid but matches no variant, serde_repr's
generated code calls `Error::custom("invalid value: N, expected one
of: ...")` directly. The previous `impl serde::de::Error for
AlpacaParseError` mapped that to `BadFormat`, which surfaced as
HTTP 400 — diverging from the Alpaca spec, which requires HTTP 200
with `ErrorNumber=0x401` (`InvalidValue`). ConformU's `TrackingRate
Write` test flagged this for `TrackingRate=5` and `TrackingRate=-1`.
Our own `deserialize_*` paths never return through `custom`: format
errors go directly to `BadFormat`, and primitive-range errors go
directly to `OutOfRange`. So everything reaching `custom` is by
construction a semantic rejection of a syntactically valid value
— ASCOM `InvalidValue`.
Add an `InvalidValue(String)` variant, route `custom` to it, and add
a response.rs arm that maps it to `ASCOMError::invalid_value`,
preserving serde_repr's "expected one of: ..." message.
234c4a2 to
e99a9a7
Compare
Update (force-pushed)
This branch was rebased onto current
mainto pick up theunnecessary_castclippy fix already onmain(commitc9ef4d5), and one additional commit was added on top:Map serde_repr variant rejections to ASCOM InvalidValue— fixes a remaining gap whereDeserialize_reprenums (e.g.DriveRate) receiving an integer that parses but matches no variant (e.g. ConformU sendingTrackingRate=5or-1) still returned HTTP 400 instead of ASCOMInvalidValue.serde_reprrejects viaError::customdirectly, so the deserializer'sserde::de::Error for AlpacaParseErrorimpl now routescustomto a newInvalidValue(String)variant, andresponse.rsmaps it toASCOMError::invalid_value. Three regression tests added.Summary
Replace
serde_plain::from_strwith a custom deserializer so Alpaca integer parameters that parse successfully but don't fit the target type return ASCOMINVALID_VALUE(error 1025) instead of HTTP 400BadRequest.Closes #5
Motivation
The ASCOM Alpaca spec and ConformU conformance tests require
INVALID_VALUEfor parseable-but-out-of-range values — e.g. sendingId=999to aSwitchwith only 4 ports should return HTTP 200 withErrorNumber: 1025, not HTTP 400.Previously,
OpaqueParams::maybe_extractusedserde_plain::from_str::<T>, which surfaces a singleParseIntErrorfor every failure mode: malformed input, negative values for unsigned targets, and values too large for the target type. Everything collapsed intoError::BadParameter→ HTTP 400, so ConformU's out-of-range checks were reported as protocol errors instead of device-level errors.How it works
Integer parsing now goes through a two-step split:
str.parse::<i64>()— fails only for genuinely malformed input (AlpacaParseError::BadFormat→ HTTP 400)T::try_from(i64_value)— fails only for values that parse as integers but don't fit the target (AlpacaParseError::OutOfRange→ ASCOMINVALID_VALUE)Error::customfrom external code (e.g.serde_reprrejecting an unknown enum variant) — surfaced asAlpacaParseError::InvalidValue→ ASCOMINVALID_VALUEi64is the narrowest intermediate that covers the full range of every Alpaca numeric target (i8/i16/i32/u8/u16/u32/usize — including u32 transaction IDs up to 4.3B and negative i32 device values), so a single genericparse_integer<T: TryFrom<i64>>handles all of them without per-type dispatch.The inner
AlpacaParseErroris carried onError::BadParameter, and theResponseWithTransaction<Result<T>>IntoResponseimpl destructures theOutOfRangeandInvalidValuevariants to produce the ASCOM-formatted error response (HTTP 200 + 1025).BadFormatcontinues to return HTTP 400.Changes
src/server/params.rs:AlpacaParseErrorenum (BadFormat/OutOfRange/InvalidValue) andAlpacaDeserializerimplementingserde::DeserializerTryFromnarrowing; float/bool/char/enum/etc. paths preservedserde::de::Error::customimpl routes toInvalidValuesoserde_reprvariant rejections (and similar external-code rejections of syntactically valid values) get ASCOMINVALID_VALUEtreatmentDeserialize_reprenum variant rejectionsrc/server/error.rs:Error::BadParameternow carriesAlpacaParseError(wasserde_plain::Error)src/server/response.rs:BadParameter { err: OutOfRange { .. } }andBadParameter { err: InvalidValue(_) }both map toASCOMError::invalid_valueCargo.toml: dropserde_plaindependency (no longer needed)Test plan
cargo check --features server,all-devicespassescargo clippy --features server,all-devicescleanRUSTFLAGS=-D warnings) clean from a clean target dirparams.rspass (13/13)Switch.Idreturns ASCOM 1025, not HTTP 400Id=abc) still returns HTTP 400TrackingRate Writetest no longer flags out-of-enum-range values (5,-1)